Building Practical Automation Scripts with Scheduling and Error Handling
Course: Python Training
Topic: Building Practical Automation Scripts with Scheduling and Error Handling
Course Link: Python Training Course at JustAcademy
Demo Link: Register for a Course Demo
Website: Visit JustAcademy
Reference Documentation: Python Scheduling Documentation | Python Logging Documentation
1. Introduction to Python Automation
Python automation is the process of using Python programs to perform repetitive tasks automatically with minimal manual intervention. Automation scripts can organize files, generate reports, process spreadsheets, send notifications, monitor application status, retrieve permitted data from APIs, and perform routine maintenance tasks.
In practical projects, an automation script should do more than execute a sequence of instructions. It should run at the appropriate time, handle unexpected situations, record useful information, and provide a reliable way to identify and recover from failures.
Scheduling and error handling are two essential components of dependable automation. Scheduling determines when a task runs, while error handling determines how the program responds when something goes wrong.
Real-World Applications
- Generating daily sales or inventory reports.
- Creating scheduled backups of important files.
- Checking application availability at regular intervals.
- Cleaning temporary files and organizing folders.
- Processing incoming CSV or Excel files.
- Sending reminders or status notifications.
- Running recurring data validation and database maintenance tasks.
- Downloading data from authorized APIs at scheduled intervals.
2. Learning Objectives
After completing this topic, learners should be able to:
- Understand the structure of a practical Python automation script.
- Use functions to organize automation logic.
- Schedule tasks using Python libraries and operating-system tools.
- Handle exceptions using
try, except, else, and finally.
- Record application events and errors with the
logging module.
- Use retries and timeouts for operations that may fail temporarily.
- Configure automation scripts to read settings from environment variables.
- Prevent duplicate task execution and improve reliability.
- Test, monitor, and troubleshoot scheduled automation jobs.
3. Understanding the Structure of an Automation Script
A maintainable automation script separates the main task from its supporting operations. For example, a report-generation program can use separate functions to read data, validate it, generate a report, save the output, and log the result.
Recommended Structure
automation_project/
main.py
tasks.py
config.py
logs/
output/
tests/
requirements.txt
| Component |
Purpose |
main.py |
Entry point that starts the automation. |
tasks.py |
Contains functions that perform the actual work. |
config.py |
Stores configuration-loading logic. |
logs/ |
Contains application log files when file logging is configured. |
output/ |
Stores generated reports or other results. |
tests/ |
Contains automated tests. |
requirements.txt |
Lists external Python dependencies. |
Example: A Simple Automation Function
from datetime import datetime
def generate_status_report():
current_time = datetime.now()
report = (
f"Automation report generated at "
f"{current_time:%Y-%m-%d %H:%M:%S}"
)
print(report)
if __name__ == "__main__":
generate_status_report()
The function contains the task logic, while the if __name__ == "__main__": block ensures that the function runs when the file is executed directly rather than simply imported as a module.
4. Why Scheduling Is Important
Scheduling allows a program to execute at a defined time or repeatedly according to a schedule. Without scheduling, a user may need to run the script manually each time the task is required.
Common Scheduling Patterns
| Pattern |
Example |
| One-time execution |
Generate a report on a specified date. |
| Fixed interval |
Check a service every five minutes. |
| Daily schedule |
Create a report every morning. |
| Weekly schedule |
Summarize transactions every Monday. |
| Monthly schedule |
Archive completed records at month-end. |
| Event-driven execution |
Process a file when it appears in an input folder. |
For production tasks, select a scheduling mechanism that fits the required precision, operating environment, and recovery needs.
5. Scheduling Tasks with time.sleep()
The built-in time module provides time.sleep(), which pauses execution for a specified number of seconds. It is useful for simple demonstrations and small scripts that need to wait between operations.
import time
def check_status():
print("Checking application status...")
while True:
check_status()
time.sleep(60)
This example performs a status check and waits approximately 60 seconds before starting the next iteration.
Limitations of time.sleep()
- The script must remain running continuously.
- The actual time between task starts can exceed the sleep interval because task execution takes time.
- It does not provide built-in calendar scheduling, persistent job storage, or automatic recovery after a process restart.
- It is not suitable by itself for every production scheduling requirement.
For a recurring task that should start approximately every minute after the previous execution finishes, a sleep loop may be sufficient. For more advanced schedules, consider a scheduler library or an operating-system scheduler.
6. Scheduling with the sched Module
Python's built-in sched module provides a basic event scheduler. It can schedule function calls to occur after a delay or at a particular point on the scheduler's time scale.
import sched
import time
from datetime import datetime
scheduler = sched.scheduler(time.time, time.sleep)
def run_task():
print(
"Task executed at:",
datetime.now().strftime("%Y-%m-%d %H:%M:%S")
)
scheduler.enter(5, 1, run_task)
print("Task scheduled for approximately 5 seconds later.")
scheduler.run()
The task is scheduled with a delay of five seconds. The call to scheduler.run() processes scheduled events and may block until the scheduled event has been handled.
Understanding Scheduler Parameters
scheduler.enter(delay, priority, action) schedules an action after a delay.
delay is the number of seconds to wait.
priority determines ordering when events are scheduled for the same time.
action is the function to execute.
The sched module is useful for lightweight scheduling, but it does not automatically provide a persistent background service or a complete calendar-based job-management system.
7. Scheduling Recurring Tasks with a Python Library
For simple recurring tasks, the third-party schedule library offers a readable syntax. Install it with:
python -m pip install schedule
Example: Run a Task Every Minute
import schedule
import time
import logging
logging.basicConfig(level=logging.INFO)
def run_report():
logging.info("Report task started")
print("Generating report...")
schedule.every(1).minutes.do(run_report)
while True:
schedule.run_pending()
time.sleep(1)
Common Scheduling Expressions
schedule.every(10).seconds.do(run_report)
schedule.every(5).minutes.do(run_report)
schedule.every().hour.do(run_report)
schedule.every().day.do(run_report)
schedule.every().monday.do(run_report)
These statements register tasks with the scheduler. The loop repeatedly checks for jobs that are due and runs them.
Important Considerations
- The Python process must continue running for scheduled jobs to execute.
- The library is intended for straightforward schedules rather than every possible production scheduling scenario.
- Long-running jobs may affect the timing of other jobs when executed synchronously.
- For exact calendar requirements, persistent schedules, distributed workloads, or more advanced controls, consider an operating-system scheduler or a suitable job-scheduling framework.
8. Using Operating-System Schedulers
Operating-system schedulers can launch Python scripts without requiring a permanent Python loop. They are useful for daily reports, backups, recurring cleanup tasks, and other jobs that should start at specific times.
Windows Task Scheduler
- Open Task Scheduler.
- Create a basic task or a task with advanced settings.
- Choose a trigger, such as daily or weekly.
- Select the action to start a program.
- Set the Python executable as the program and your script path as an argument.
- Configure the working directory, permissions, and failure behavior as required.
- Test the task and inspect its execution history.
Example configuration values:
Program:
C:\Path\To\Python\python.exe
Arguments:
C:\Projects\automation\main.py
Start in:
C:\Projects\automation
Use the actual Python executable and project paths from your environment. If your project uses a virtual environment, configure the task to use that environment's Python executable.
Linux and macOS with cron
On many Unix-like systems, cron can launch scripts according to a recurring schedule. Edit the current user's crontab with:
crontab -e
Example: run a Python script every day at 8:00 AM.
0 8 * * * /usr/bin/python3 /home/user/automation/main.py
The five schedule fields represent minute, hour, day of month, month, and day of week.
| Expression |
Meaning |
0 8 * * * |
Every day at 8:00 AM. |
*/15 * * * * |
Every 15 minutes. |
0 9 * * 1 |
Every Monday at 9:00 AM. |
30 18 * * 1-5 |
At 6:30 PM Monday through Friday. |
Check the scheduler's timezone, the machine's local time, the Python path, file permissions, environment variables, and working directory. Cron commonly runs with a more limited environment than an interactive terminal.
9. Introduction to Error Handling
Error handling allows a program to respond to exceptional conditions instead of stopping unexpectedly without a useful explanation. Automation scripts often depend on files, network connections, external services, and databases, so failures must be anticipated.
Common Automation Errors
FileNotFoundError: An expected file does not exist.
PermissionError: The script does not have sufficient access permissions.
ValueError: A value cannot be converted or interpreted as expected.
TimeoutError: An operation exceeds its allowed time.
ConnectionError: A network connection fails.
KeyError: A required dictionary key is missing.
ZeroDivisionError: A calculation attempts to divide by zero.
- Application-specific exceptions: A task fails because of a business rule or external dependency.
10. Using try and except
The try block contains code that may raise an exception. An except block handles a matching exception.
def divide_numbers(a, b):
try:
result = a / b
print("Result:", result)
except ZeroDivisionError:
print("Cannot divide by zero.")
divide_numbers(10, 2)
divide_numbers(10, 0)
Output:
Result: 5.0
Cannot divide by zero.
In automation, catch exceptions that you can meaningfully handle. Avoid using a broad exception handler that silently hides every error.
Handling Multiple Exceptions
def convert_to_integer(value):
try:
number = int(value)
return number
except (ValueError, TypeError) as error:
print(f"Invalid integer value: {error}")
return None
print(convert_to_integer("25"))
print(convert_to_integer("hello"))
Grouping related exceptions is appropriate when the same recovery behavior applies to all of them.
11. Understanding else and finally
The else block runs when the try block completes without raising a handled exception. The finally block runs during normal completion and when an exception propagates, making it useful for cleanup.
def read_file(file_path):
file = None
try:
file = open(file_path, "r", encoding="utf-8")
content = file.read()
except OSError as error:
print(f"Unable to read file: {error}")
else:
print("File read successfully.")
print(content)
finally:
if file is not None:
file.close()
print("File operation finished.")
For ordinary file operations, a context manager is usually cleaner because it closes the file automatically.
def read_file(file_path):
try:
with open(file_path, "r", encoding="utf-8") as file:
content = file.read()
except OSError as error:
print(f"Unable to read file: {error}")
return None
else:
print("File read successfully.")
return content
12. Raising Custom Exceptions
Sometimes a task encounters an invalid condition that Python cannot detect automatically. Use raise to indicate that the operation cannot continue according to the application's rules.
def validate_record(record):
if "name" not in record:
raise ValueError("Record must contain a name.")
if not record["name"].strip():
raise ValueError("Name cannot be empty.")
return True
sample_record = {"name": "Inventory Report"}
try:
validate_record(sample_record)
print("Record is valid.")
except ValueError as error:
print(f"Validation failed: {error}")
For larger applications, define custom exception classes when different failure types require different handling or reporting.
class AutomationError(Exception):
"""Base exception for automation failures."""
class DataValidationError(AutomationError):
"""Raised when input data is invalid."""
13. Logging Automation Events
Logging records information about a program's execution. Unlike temporary print statements, logging supports severity levels, timestamps, structured messages, and output destinations such as files or the console.
Common Logging Levels
| Level |
Purpose |
Example |
DEBUG |
Detailed information for diagnosing problems. |
Values used during a calculation. |
INFO |
Normal application progress. |
Task started or report generated. |
WARNING |
An unusual condition that may not stop execution. |
An optional file is missing. |
ERROR |
An operation failed. |
Unable to process an input file. |
CRITICAL |
A severe failure affecting the application. |
A required subsystem is unavailable. |
Writing Logs to a File
import logging
logging.basicConfig(
filename="automation.log",
level=logging.INFO,
format="%(asctime)s - %(levelname)s - %(message)s",
encoding="utf-8"
)
logging.info("Automation script started.")
logging.warning("Optional configuration was not found.")
logging.error("A sample operation failed.")
The log file records the events that occur after logging is configured. The file is generally created when the logging system writes its first message, provided the destination is accessible.
Logging Exceptions with Tracebacks
import logging
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s - %(levelname)s - %(message)s"
)
try:
result = 10 / 0
except ZeroDivisionError:
logging.exception("Calculation failed.")
logging.exception() should normally be called inside an exception handler. It records an error message together with the exception traceback, which helps diagnose the failure.
14. Building a Reliable Task Function
A scheduled task should have a clear boundary. Its function should perform one meaningful unit of work and either finish successfully or report failure through an exception or a well-defined result.
import logging
from datetime import datetime
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s - %(levelname)s - %(message)s"
)
def generate_report():
logging.info("Starting report generation.")
try:
report_time = datetime.now().isoformat(timespec="seconds")
report = f"Report generated at {report_time}"
with open(
"report.txt",
"w",
encoding="utf-8"
) as file:
file.write(report + "\n")
except OSError:
logging.exception("Unable to write report.")
raise
else:
logging.info("Report generated successfully.")
if __name__ == "__main__":
generate_report()
This script logs task progress, writes a report, and re-raises file errors after recording them. Re-raising is useful when an external scheduler or monitoring system must be able to detect that the process failed.
15. Adding Retry Logic
Some failures are temporary. A remote service might briefly become unavailable, or a network connection might time out. A retry strategy allows the program to try an operation again before reporting a final failure.
Retries should be limited to operations that are safe to repeat. Repeating a non-idempotent operation, such as submitting a payment or creating a duplicate order, can cause unwanted side effects unless the operation has suitable safeguards.
Example: Retry a Temporary Operation
import logging
import time
logging.basicConfig(level=logging.INFO)
def perform_operation():
# Replace with an operation that can safely be retried.
print("Performing operation...")
def run_with_retries(max_attempts=3, delay_seconds=2):
for attempt in range(1, max_attempts + 1):
try:
perform_operation()
logging.info("Operation succeeded.")
return True
except (TimeoutError, ConnectionError) as error:
logging.warning(
"Attempt %s of %s failed: %s",
attempt,
max_attempts,
error
)
if attempt == max_attempts:
logging.error("All retry attempts failed.")
raise
time.sleep(delay_seconds)
return False
if __name__ == "__main__":
run_with_retries()
In this demonstration, perform_operation() is a placeholder and does not intentionally raise an error. Replace it with a real operation and catch only exceptions that may reasonably be temporary.
Exponential Backoff
Instead of waiting the same amount of time between attempts, exponential backoff increases the delay after each failure. It can reduce pressure on a struggling service.
delay = 1
for attempt in range(3):
try:
perform_operation()
break
except (TimeoutError, ConnectionError):
if attempt == 2:
raise
time.sleep(delay)
delay *= 2
This example waits approximately one second before the second attempt and two seconds before the third. Production systems may also use randomized jitter and respect a remote service's retry guidance.
16. Handling Timeouts in Network Automation
Network operations should generally have explicit timeouts so a scheduled job does not wait indefinitely for a response.
import logging
import requests
logging.basicConfig(level=logging.INFO)
def check_website(url):
try:
response = requests.get(url, timeout=10)
response.raise_for_status()
logging.info(
"Request successful. Status code: %s",
response.status_code
)
return True
except requests.exceptions.Timeout:
logging.error("Request timed out.")
except requests.exceptions.RequestException:
logging.exception("Network request failed.")
return False
if __name__ == "__main__":
check_website("https://example.com")
The timeout limits how long the request waits for network operations. Requests supports separate connection and read timeout values when more precise control is needed. The example URL is a reserved demonstration domain.
17. Preventing Overlapping Scheduled Jobs
An overlapping job occurs when a new execution starts before a previous execution has finished. This can happen when a scheduled interval is shorter than the time needed to complete the task, or when multiple scheduler instances launch the same script.
Potential Problems
- Two processes may write to the same file simultaneously.
- Database updates may conflict.
- Duplicate emails or reports may be produced.
- System resources may be consumed unnecessarily.
Basic In-Process Guard
import logging
import threading
import time
logging.basicConfig(level=logging.INFO)
task_lock = threading.Lock()
def scheduled_task():
if not task_lock.acquire(blocking=False):
logging.warning("Previous task is still running.")
return
try:
logging.info("Task started.")
time.sleep(3) # Simulate work.
logging.info("Task finished.")
finally:
task_lock.release()
This lock prevents overlapping calls that share the same lock object within the same Python process. It does not protect against a second process or another machine. For cross-process protection, use an appropriate operating-system lock, database lock, job queue, or distributed coordination mechanism.
18. Making Automation Idempotent
An operation is idempotent when repeating it produces the same intended final state as performing it once. Idempotency is especially important when a scheduler retries a failed task or reruns a job after a restart.
Examples
| Operation |
Potentially Idempotent Approach |
| Creating a report |
Generate a report for a defined date and replace the corresponding output file. |
| Updating a record |
Set the record to a specific value instead of applying the same increment repeatedly. |
| Processing an input file |
Track processed file identifiers and skip completed inputs. |
| Sending a transaction request |
Use a supported idempotency key or equivalent provider mechanism. |
Idempotency depends on the complete operation, including external side effects. Replacing a local report file may be idempotent, while sending a notification on every run is not necessarily idempotent.
19. Managing Configuration with Environment Variables
Configuration values such as file paths, service endpoints, and retry limits should be separated from application logic. Environment variables are useful for supplying configuration without hardcoding deployment-specific values in the source code.
import os
OUTPUT_DIR = os.getenv("AUTOMATION_OUTPUT_DIR", "output")
RETRY_LIMIT = int(os.getenv("AUTOMATION_RETRY_LIMIT", "3"))
print("Output directory:", OUTPUT_DIR)
print("Retry limit:", RETRY_LIMIT)
Set the environment variables through the operating system, deployment environment, or an approved configuration mechanism. Validate values because environment variables are strings and may contain invalid input.
Validation Example
import os
def get_retry_limit():
raw_value = os.getenv("AUTOMATION_RETRY_LIMIT", "3")
try:
retry_limit = int(raw_value)
except ValueError as error:
raise ValueError(
"AUTOMATION_RETRY_LIMIT must be an integer."
) from error
if retry_limit < 1:
raise ValueError(
"AUTOMATION_RETRY_LIMIT must be at least 1."
)
return retry_limit
print(get_retry_limit())
Do not commit passwords, API tokens, or other secrets to source control. Use a suitable secret-management mechanism and grant automation processes only the permissions they need.
20. Writing a Complete Scheduled Automation Script
The following example creates a timestamped report, logs task activity, catches file-related failures, and schedules the task using the schedule library.
Install the dependency first:
python -m pip install schedule
Complete Example
import logging
import os
import time
from datetime import datetime
from pathlib import Path
import schedule
OUTPUT_DIR = Path(
os.getenv("AUTOMATION_OUTPUT_DIR", "output")
)
LOG_FILE = os.getenv(
"AUTOMATION_LOG_FILE",
"automation.log"
)
logging.basicConfig(
filename=LOG_FILE,
level=logging.INFO,
format="%(asctime)s - %(levelname)s - %(message)s",
encoding="utf-8"
)
def generate_report():
logging.info("Report task started.")
try:
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
today = datetime.now().strftime("%Y-%m-%d")
report_path = OUTPUT_DIR / f"report-{today}.txt"
report_content = (
"Daily Automation Report\n"
f"Generated at: {datetime.now().isoformat(timespec='seconds')}\n"
"Status: Completed\n"
)
with report_path.open("w", encoding="utf-8") as file:
file.write(report_content)
except OSError:
logging.exception("Report generation failed.")
raise
else:
logging.info("Report saved to %s", report_path)
def run_scheduled_task():
try:
generate_report()
except OSError:
# Log the failure and allow the scheduler loop to continue.
# A monitoring system should still be configured to alert on it.
logging.error("Scheduled execution failed.")
def main():
logging.info("Automation service starting.")
schedule.every().day.at("08:00").do(run_scheduled_task)
# Optional initial run. Remove this line if the task
# should only run at its scheduled time.
run_scheduled_task()
while True:
schedule.run_pending()
time.sleep(1)
if __name__ == "__main__":
main()
How the Script Works
- Configuration values are read from environment variables, with defaults where appropriate.
- Logging records task starts, successful completions, and failures.
- The report directory is created if it does not already exist.
- A report file is generated for the current date.
- File-system errors are recorded and propagated from the report function.
- The scheduled wrapper catches the expected file-system failure so the simple scheduling loop can continue.
- The task is scheduled for 8:00 AM according to the scheduler's local-time behavior.
Important: This is a learning example. The process must remain running, and the task runs once immediately because of the explicit initial call. Remove that call if only scheduled execution is wanted. The script also overwrites the same day's report file if it runs more than once that day. In a production system, add monitoring, alerting, retention rules, process supervision, and any required protection against duplicate execution.
21. Testing Automation Scripts
Testing helps verify that the task works under both normal and failure conditions before it is scheduled in a live environment.
What Should Be Tested?
- Successful completion using valid input.
- Missing input files and invalid data.
- Permission errors and unavailable output directories.
- Network timeouts and unsuccessful HTTP responses.
- Retry limits and final failure behavior.
- Scheduled execution at the expected time.
- Duplicate execution and overlapping-task scenarios.
- Logging output and recovery behavior.
Example Unit Test with unittest
import unittest
def validate_amount(value):
if value < 0:
raise ValueError("Amount cannot be negative.")
return value
class TestValidation(unittest.TestCase):
def test_valid_amount(self):
self.assertEqual(validate_amount(10), 10)
def test_negative_amount(self):
with self.assertRaises(ValueError):
validate_amount(-1)
if __name__ == "__main__":
unittest.main()
Save the code in a test file and run it with Python. Unit tests can verify task functions independently of the scheduler, while integration tests can check the complete workflow.
22. Monitoring and Troubleshooting Scheduled Jobs
A script can work correctly when run manually and still fail under a scheduler. This often happens because the scheduled environment has a different working directory, Python interpreter, user account, set of permissions, or environment variables.
| Problem |
Likely Cause |
Recommended Solution |
| Script does not start |
Incorrect executable or script path. |
Use absolute paths and test the exact command manually. |
| Module import failure |
Scheduler uses a different Python environment. |
Configure the intended virtual environment's Python executable. |
| Output file is missing |
Unexpected working directory or permissions. |
Use explicit paths and verify directory access. |
| Task runs at the wrong time |
Timezone or daylight-saving differences. |
Verify the scheduler's timezone configuration and requirements. |
| Repeated output or duplicate actions |
Multiple scheduler instances or overlapping jobs. |
Use locking, idempotency, and a single-job execution policy. |
| Job stops after an error |
An exception escaped the main loop or the process exited. |
Handle expected task failures and configure external process supervision. |
| No error information is available |
Logs are missing, inaccessible, or incomplete. |
Configure file or centralized logging and monitor job exit status. |
23. Security and Reliability Best Practices
- Use absolute paths for important input, output, and configuration files.
- Use explicit timeouts for network and other potentially blocking operations.
- Catch specific exceptions and avoid silently ignoring failures.
- Log enough context to diagnose a failure without recording passwords or sensitive data.
- Keep API keys and passwords out of source code and log files.
- Restrict the permissions of the account that runs the automation.
- Use retries only for appropriate transient errors and limit the number of attempts.
- Make repeated runs safe whenever possible.
- Protect shared files and resources from concurrent writes.
- Use log rotation or centralized logging for long-running services.
- Monitor failed executions and configure alerts for important tasks.
- Test failure scenarios and recovery behavior before production deployment.
- Ensure the system has a clear strategy for restarts, missed runs, and partially completed work.
24. Practice Exercises
Exercise 1: Scheduled Message
Write a Python script that prints a message every minute. Record the time of each execution and test what happens when you stop the script.
Exercise 2: File Backup
Create a script that copies a permitted source file into a backup directory. Handle missing files, permission errors, and invalid destination paths.
Exercise 3: Daily Report
Generate a text report containing the current date, a task summary, and the number of processed records. Save it to an output directory and log successful completion.
Exercise 4: Retry a Temporary Failure
Create a function that simulates a temporary timeout. Retry the operation up to three times and record every attempt in a log file.
Exercise 5: Error Logging
Build a program that reads a CSV file and validates its rows. Log invalid rows with enough context to diagnose the problem without exposing sensitive information.
Exercise 6: Operating-System Scheduling
Configure a simple script to run daily using Windows Task Scheduler or cron. Verify its working directory, output path, execution time, and error logs.
25. Mini Project: Automated Daily File Report
Project Objective: Build a dependable Python script that scans a designated directory, counts its files, and generates a daily summary report.
Project Requirements
- Read the source directory from an environment variable or a configuration setting.
- Check whether the directory exists and is accessible.
- Count files without modifying the source files.
- Write a timestamped summary report.
- Log task starts, successful completion, and failures.
- Use error handling for file-system operations.
- Schedule the script through an appropriate scheduler.
Sample Implementation
import logging
import os
from datetime import datetime
from pathlib import Path
SOURCE_DIR = Path(
os.getenv("AUTOMATION_SOURCE_DIR", "input")
)
OUTPUT_DIR = Path(
os.getenv("AUTOMATION_OUTPUT_DIR", "output")
)
logging.basicConfig(
filename="file_report.log",
level=logging.INFO,
format="%(asctime)s - %(levelname)s - %(message)s",
encoding="utf-8"
)
def generate_file_report():
logging.info("File report started.")
try:
if not SOURCE_DIR.is_dir():
raise FileNotFoundError(
f"Source directory does not exist: {SOURCE_DIR}"
)
file_count = sum(
1 for path in SOURCE_DIR.iterdir()
if path.is_file()
)
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
timestamp = datetime.now().strftime("%Y%m%d-%H%M%S")
report_path = OUTPUT_DIR / f"file-report-{timestamp}.txt"
report = (
"File Summary Report\n"
f"Generated: {datetime.now().isoformat(timespec='seconds')}\n"
f"Source directory: {SOURCE_DIR.resolve()}\n"
f"Number of files: {file_count}\n"
)
with report_path.open("w", encoding="utf-8") as file:
file.write(report)
except (OSError, FileNotFoundError):
logging.exception("Unable to generate file report.")
raise
else:
logging.info("Report created: %s", report_path)
return report_path
if __name__ == "__main__":
try:
generate_file_report()
except (OSError, FileNotFoundError):
raise SystemExit(1)
The script returns a nonzero process exit status when the report fails. This allows external schedulers and monitoring systems to detect a failure. The timestamped filename also reduces the chance of unintentionally overwriting an earlier report.
Possible Improvements
- Add a summary of file sizes and file types.
- Exclude temporary files or subdirectories according to defined rules.
- Add unit tests for missing directories and output failures.
- Schedule the script at an agreed daily time.
- Configure alerts when the job fails or produces unexpected results.
- Apply log rotation and a retention policy for old reports.
26. Interview Questions and Answers
Q1. What is Python automation?
Answer: Python automation uses scripts to perform repetitive or scheduled tasks with minimal manual intervention.
Q2. Why is scheduling important?
Answer: Scheduling allows tasks to execute at predetermined times or intervals without requiring a user to start them manually.
Q3. What is the difference between time.sleep() and a scheduler?
Answer: time.sleep() pauses execution for a duration. A scheduler manages tasks according to registered times or intervals and may provide more convenient scheduling features.
Q4. What is exception handling?
Answer: Exception handling allows a program to respond to errors using constructs such as try, except, else, and finally.
Q5. Why should automation scripts use logging?
Answer: Logging provides a record of task progress, warnings, errors, and diagnostic details that can be used to troubleshoot scheduled executions.
Q6. What is the purpose of a retry mechanism?
Answer: A retry mechanism attempts an operation again when a suitable temporary failure occurs. Retries should be limited and used only when repeating the operation is safe.
Q7. What is exponential backoff?
Answer: Exponential backoff increases the waiting period between retry attempts, helping reduce repeated pressure on a failing service.
Q8. What is idempotency in automation?
Answer: Idempotency means that repeating an operation produces the same intended final state as performing it once, making retries and reruns safer.
Q9. How can you prevent overlapping tasks?
Answer: Use locks, a single-instance execution policy, a job queue, or another suitable coordination mechanism to prevent concurrent executions from interfering with each other.
Q10. Why might a scheduled script fail when it works manually?
Answer: The scheduler may use a different Python executable, working directory, environment variables, permissions, or timezone.
Q11. Why are timeouts important?
Answer: Timeouts limit how long an operation waits, preventing a task from hanging indefinitely on an unresponsive dependency.
Q12. How can a scheduler detect a failed task?
Answer: A scheduled process can report failure through its exit status, logs, monitoring integration, or configured alerting system.
Q13. What is the difference between logging an exception and re-raising it?
Answer: Logging records diagnostic information. Re-raising propagates the exception to the caller, allowing higher-level code or an external scheduler to respond to the failure.
Q14. What should be considered before retrying an operation?
Answer: Determine whether the error is temporary, whether repeating the operation is safe, how many attempts are appropriate, and whether delays or service-specific retry instructions should be followed.
27. Quick Revision Checklist
- Understand the structure and purpose of an automation script.
- Separate task logic into reusable functions.
- Choose between a sleep loop, Python scheduler, or operating-system scheduler.
- Use
try and except for expected failures.
- Use
finally or context managers for reliable cleanup.
- Log progress and failures with useful context.
- Set timeouts for network and blocking operations.
- Use bounded retries for suitable transient failures.
- Prevent overlapping executions when tasks share resources.
- Design operations to be idempotent whenever possible.
- Validate configuration and protect secrets.
- Test scheduled execution and verify logs, exit status, and recovery behavior.
28. Conclusion
Building practical Python automation scripts requires more than writing code that performs a task. A reliable solution must run at the right time, respond safely to errors, record its execution history, and recover appropriately from temporary failures.
By combining reusable functions, scheduling tools, exception handling, logging, timeouts, retries, and testing, developers can create automation workflows that are easier to maintain and troubleshoot. Start with a small task, test normal and failure scenarios, and then add the scheduling and monitoring mechanisms required for the deployment environment.
To explore more Python programming concepts, visit the JustAcademy Python Training Course, register for a course demo, or explore the JustAcademy website.